"""
This file tracks the source of cached data and listens to the appropriate
signals for object modification.
"""

from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.db.models import signals
from django.dispatch import dispatcher

# Python 2.3 compatibility
if not hasattr(__builtins__, 'set'):
    from sets import Set as set

registered_models = {}

# TODO: This module needs investigation into performance. (Currently O(n*m)?)
#       Avoiding premature optimization (for now ;)... See mailing list topic.

def get_model_id(model):
    """ContentType caches this for us in memory, so this isn't expensive."""
    # Hmm, it'd be nice if models had a convenient method for retrieving this..
    return ContentType.objects.get_for_model(model).id

def register_set(queryset, cache_key):
    """Called when a particular cache_set() is used."""
    reg = register_model(queryset.model)
    if cache_key not in reg.cached_sets:
        # First time this has been called!
        reg.cached_sets.add(cache_key)
        # Add this set to the list of affected sets for each pk.
        for obj in queryset:
            affected_sets = reg.cached_pks.setdefault(obj._get_pk_val(), set())
            affected_sets.add(cache_key)


def register_model(model):
    """Sets up the appropriate signals to update cache for the given model.
    
    Returns the ModelRegistration."""
    model_id = get_model_id(model)
    if model_id in registered_models: # Already registered?
        return registered_models[model_id]
    # Put the model in our registry and connect the signals up
    registered_models[model_id] = reg = ModelRegistration(model)
    #dispatcher.connect(reg.post_init, signal=signals.post_init, sender=model)
    dispatcher.connect(reg.pre_save, signal=signals.pre_save, sender=model)
    dispatcher.connect(reg.post_save, signal=signals.post_save, sender=model)
    dispatcher.connect(reg.pre_delete, signal=signals.pre_delete, sender=model)
    return reg


class ModelRegistration(object):
    def __init__(self, model):
        self.model = model
        self.model_id = get_model_id(model) # A little redundant...
        # cache_set()'d QuerySets registered with this model
        self.cached_sets = set()

        # This dict maps object pks to a set containing the cache keys that
        # point to QuerySets that contain said pks. Ouch.
        # TODO: Something, anything!
        self.cached_pks = {}

    def pre_save(self, sender, instance, signal):
        """Catches creation of new model instances."""
        if instance._get_pk_val() is None:
            # New object created.
            # TODO: Determine which sets are actually affected (tricky!)
            #       For now, delete *everything*
            for set_key in self.cached_sets:
                cache.delete(set_key)
            self.cached_sets = set()

    def post_save(self, sender, instance, signal): 
        """Catches model instance modification."""
        pk = instance._get_pk_val()
        for set_key in self.cached_pks.get(pk, []):
            old_set = cache.get(set_key)
            if old_set is None: # This set is expired?
                # XXX: Remove this cache_key from the QS set!!
                continue
            # TODO: Locate the pk efficiently. Or just delete the key for now?
            for ctr, instance in enumerate(old_set):
                if instance._get_pk_val() == pk:
                    # Replace the old value with the new and resave the set.
                    # Have to profile this...
                    old_set[ctr] = instance
                    cache.set(set_key, old_set)
                    break
            else:
                assert False # pk should have been present.

    def pre_delete(self, sender, instance, signal):
        """Catches model instance deletion."""
        pk = instance._get_pk_val()
        # Go through all the sets that contain this pk
        for set_key in self.cached_pks.get(pk, []):
            old_set = cache.get(set_key)
            if old_set is None: # This set is expired?
                # XXX: Remove this cache_key from the QS set!!!
                continue
            for ctr, instance in enumerate(old_set):
                if instance._get_pk_val() == pk:
                    # Delete the removed value and re-save the set.
                    # TODO: Detect multiple-deletion and handle that in
                    # delete() instead
                    del old_set[ctr]
                    cache.set(set_key, old_set)
                    break
            else:
                assert False # It wasn't there? Something's wrong

        # Finally, clear the list of sets that reference this pk
        if pk in self.cached_pks:
            self.cached_pks[pk] = []

